import { Matrix4, Quaternion, Ray, Vector3, type Camera, MathUtils } from "three";
import { Ellipsoid } from "./Ellipsoid";

const EPSILON = 0.000001;

const eastScratch = /*#__PURE__*/ new Vector3();
const northScratch = /*#__PURE__*/ new Vector3();
const upScratch = /*#__PURE__*/ new Vector3();
const vectorScratch1 = /*#__PURE__*/ new Vector3();
const vectorScratch2 = /*#__PURE__*/ new Vector3();
const vectorScratch3 = /*#__PURE__*/ new Vector3();
const matrixScratch = /*#__PURE__*/ new Matrix4();
const quaternionScratch = /*#__PURE__*/ new Quaternion();
const rayScratch = /*#__PURE__*/ new Ray();

export class PointOfView {
  // Distance from the target.
  private _distance!: number;

  // Radians from the local east direction relative from true north, measured
  // clockwise (90 degrees is true north, and -90 is true south).
  heading: number;

  // Radians from the local horizon plane, measured with positive values looking
  // up (90 degrees is straight up, -90 is straight down).
  private _pitch!: number;

  roll: number;

  constructor(distance = 0, heading = 0, pitch = 0, roll = 0) {
    this.distance = distance;
    this.heading = heading;
    this.pitch = pitch;
    this.roll = roll;
  }

  get distance(): number {
    return this._distance;
  }

  set distance(value: number) {
    this._distance = Math.max(value, EPSILON);
  }

  get pitch(): number {
    return this._pitch;
  }

  set pitch(value: number) {
    this._pitch = MathUtils.clamp(value, -Math.PI / 2 + EPSILON, Math.PI / 2 - EPSILON);
  }

  set(distance: number, heading: number, pitch: number, roll?: number): this {
    this.distance = distance;
    this.heading = heading;
    this.pitch = pitch;
    if (roll != null) {
      this.roll = roll;
    }
    return this;
  }

  clone(): PointOfView {
    return new PointOfView(this.distance, this.heading, this.pitch, this.roll);
  }

  copy(other: PointOfView): this {
    this.distance = other.distance;
    this.heading = other.heading;
    this.pitch = other.pitch;
    this.roll = other.roll;
    return this;
  }

  equals(other: PointOfView): boolean {
    return (
      other.distance === this.distance &&
      other.heading === this.heading &&
      other.pitch === this.pitch &&
      other.roll === this.roll
    );
  }

  decompose(
    target: Vector3,
    eye: Vector3,
    quaternion: Quaternion,
    up?: Vector3,
    ellipsoid = Ellipsoid.WGS84
  ): void {
    ellipsoid.getEastNorthUpVectors(target, eastScratch, northScratch, upScratch);
    up?.copy(upScratch);

    // h = east * cos(heading) + north * sin(heading)
    // v = h * cos(pitch) + up * sin(pitch)
    const offset = vectorScratch1
      .copy(eastScratch)
      .multiplyScalar(Math.cos(this.heading))
      .add(vectorScratch2.copy(northScratch).multiplyScalar(Math.sin(this.heading)))
      .multiplyScalar(Math.cos(this.pitch))
      .add(vectorScratch2.copy(upScratch).multiplyScalar(Math.sin(this.pitch)))
      .normalize()
      .multiplyScalar(this.distance);
    eye.copy(target).sub(offset);

    if (this.roll !== 0) {
      const rollAxis = vectorScratch1.copy(target).sub(eye).normalize();
      upScratch.applyQuaternion(quaternionScratch.setFromAxisAngle(rollAxis, this.roll));
    }
    quaternion.setFromRotationMatrix(matrixScratch.lookAt(eye, target, upScratch));
  }

  setFromCamera(camera: Camera, ellipsoid = Ellipsoid.WGS84): this | undefined {
    const eye = vectorScratch1.setFromMatrixPosition(camera.matrixWorld);
    const direction = vectorScratch2.set(0, 0, 0.5).unproject(camera).sub(eye).normalize();
    const target = ellipsoid.getIntersection(rayScratch.set(eye, direction));
    if (target == null) {
      return;
    }

    this.distance = eye.distanceTo(target);
    ellipsoid.getEastNorthUpVectors(target, eastScratch, northScratch, upScratch);
    this.heading = Math.atan2(northScratch.dot(direction), eastScratch.dot(direction));
    this.pitch = Math.asin(upScratch.dot(direction));

    // Need to rotate camera's up to evaluate it in world space.
    const up = vectorScratch1.copy(camera.up).applyQuaternion(camera.quaternion);
    const s = vectorScratch3.copy(direction).multiplyScalar(-up.dot(direction)).add(up).normalize();
    const t = vectorScratch1
      .copy(direction)
      .multiplyScalar(-upScratch.dot(direction))
      .add(upScratch)
      .normalize();
    const x = t.dot(s);
    const y = direction.dot(t.cross(s));
    this.roll = Math.atan2(y, x);

    return this;
  }
}
